跳到主要内容

TypeScript type 和 interface 的异同及其使用

子类型的分类

编程语言的子类型分为两种:名义子类型和结构子类型。

名义子类型就是指,例如 Java 中类的继承,子类就是父类的子类型,而要建立父子类的关系只有一个办法就是 extends(接口的 implements 也算),只有用了 extends 才会出现父子类型,也就是只能用 extends 才能让他们成为名义上的父子类型。

结构子类型就是结构相同即可,而 TypeScript 就是结构子类型,即在 TypeScript 中

type Foo = {
a: string
}

// 和

type Bar = {
a: string
}

// 这两个都是一样类型

不需要 extends(尽管 extends 也可以创造子类型,但本质是结构相似)。所以

type Foo = {
a: string
}
type Bar = {
a: string,
b: number
}

中 Bar 是 Foo 的子类型。

相似的地方

对象自变量式的结构定义

interface Foo {
a: string
}

type Foo {
a: string
}

都是定义了一个有 a 属性的对象结构。

函数类型

函数类型其实由两个部分构成,参数类型和返回值类型。

interface Foo {
(a: string): string
}

type Foo = (a: string) => string

混合类型

由于 JavaScript 具有动态和灵活的性质,有时可能会遇到一个对象,该对象可以作为上述某些类型的组合使用,就是一个既具有函数特性又具有对象特性的类型,它具有一些属性。如下:

interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

function getCounter(): Counter {
let counter = (function (start: number) { }) as Counter;
counter.interval = 123;
counter.reset = function () { };
return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

当然其中的

interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

也可以使用 type 实现 :

type Counter = {
(start: number): string;
interval: number;
reset(): void;
}

关于 | 和 & 操作符的使用

type Foo = {
a: string
}
type Bar = {
b: number
}
type Baz = Foo & Bar

interface Foo {
a: string
}
interface Bar {
b: number
}
type Baz = Foo & Bar

同样 | 操作符也是

&| 操作符都会创建一个新的类型,而且是相关类型父子类型链上的类型。它们区别是他们的产物是在父子类型链上的不同角色。如上所示,第一段代码中,对 Foo 和 Bar 使用了 & 操作符,创建的新的类型 Baz,结果是 Baz 是 Foo 和 Bar 的子类型,就是说,Bar 类型的变量何以赋值给 Foo 和 Bar 类型的变量。在第二段代码中,对 Foo 和 Bar 使用了|操作符,创建的新的类型 Baz,结果是 Baz 是 Foo 和 Bar 的父类型,即,Foo 和 Bar 类型的变量可以赋值给 Bar。

索引类型

interface Foo {
[x: string]: number
[x: number]: string
}

type Foo = {
[x: string]: number
[x: number]: string
}

都表示索引为 string 的属性的类型都为 number,索引为 number 的属性的类型都为 string。

不同的地方

type 类型别称

很简单,就是为已经存在的类型创建另一个名字,代表完全相同的意义。例如:

type ObjectAlias = object

虽然说这个特性是 type 独有的,但当原类型不是原始类型时,即原类型不是 number、string、boolean、object、symbol、null、undefined、void、never、unknown、any 时,interface 可以使用以下方式实现类似的功能:

interface Foo {
a: string
}
interface FooAlias extends Foo {}

扩展接口

interface Foo {
a: string
}
interface Bar extends Foo {
b: number
}

type 使用 & 可以实现类似的效果,如下:

interface Foo {
a: string
}
type Bar = Foo & {
b: number
}

补充:接口的构造函数

构造签名

在 TypeScript 接口中,你可以使用 new 关键字来描述一个构造函数:

interface Point {
new (x: number, y: number): Point;
}

以上接口中的 new (x: number, y: number) 我们称之为构造签名

与该语法相对应的几种常见的使用形式如下:

new C  
new C ( ... )
new C < ... > ( ... )

构造函数类型

构造函数类型字面量是包含单个构造函数签名的对象类型的简写。具体来说,构造函数类型字面量的形式如下:

new < T1, T2, ... > ( p1, p2, ... ) => R

该形式与以下对象字面量类型是等价的:

{ new < T1, T2, ... > ( p1, p2, ... ) : R }

下面我们来举个实际的示例:

// 构造函数字面量
new (x: number, y: number) => Point

等价于以下对象类型字面量:

{
new (x: number, y: number): Point;
}

构造函数类型的应用

在介绍构造函数类型的应用前,我们先来看个例子:

interface Point {
new (x: number, y: number): Point;
x: number;
y: number;
}

class Point2D implements Point {
readonly x: number;
readonly y: number;

constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

const point: Point = new Point2D(1, 2);

对于以上的代码,TypeScript 编译器会提示以下错误信息:

Class 'Point2D' incorrectly implements interface 'Point'.
Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.

要解决这个问题,我们就需要把对前面定义的 Point 接口进行分离,即把接口的属性和构造函数类型进行分离:

interface Point {
x: number;
y: number;
}

interface PointConstructor {
new (x: number, y: number): Point;
}

完成接口拆分之后,除了前面已经定义的 Point2D 类之外,我们又定义了一个 newPoint 工厂函数,该函数用于根据传入的 PointConstructor 类型的构造函数,来创建对应的 Point 对象。

class Point2D implements Point {
readonly x: number;
readonly y: number;

constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

function newPoint(
pointConstructor: PointConstructor,
x: number,
y: number
): Point {
return new pointConstructor(x, y);
}

const point: Point = newPoint(Point2D, 2, 2);

Reference

TS 的构造签名和构造函数类型是啥?傻傻分不清楚 TypeScript(二):type & interface